package com.canyou.notify;

import de.codecentric.boot.admin.server.domain.entities.Instance;
import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
import de.codecentric.boot.admin.server.domain.values.StatusInfo;
import de.codecentric.boot.admin.server.notify.AbstractStatusChangeNotifier;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * 钉钉提醒应用状态变更
 *
 * @author fy
 * @date 19/10/25 13:54
 */
@Slf4j
public class DingTalkNotifier extends AbstractStatusChangeNotifier {
    private static final String EX_KEY = "exception";
    private static final String ER_KEY = "error";

    private static final String DEFAULT_MESSAGE =
            "### 告警信息\n" +
                    "> 应用: **#{instance.registration.name}** (#{instance.id}) 状态变更为 **#{event.statusInfo.status}** [请注意排查](#{monitorUrl}/#/instances/#{instance.id}/details)\n\n" +
                    "> 详细信息: \n\n" +
                    "> #{statusInfo}";
    private final SpelExpressionParser parser = new SpelExpressionParser();
    private RestTemplate restTemplate = new RestTemplate();
    private String msgType = "markdown";
    private String title = "应用告警";

    /**
     * 钉钉调用地址
     */
    private String apiUrl = "https://oapi.dingtalk.com/robot/send";

    /**
     * 授权token
     */
    private String accessToken;

    /**
     * 需要@的人
     */
    private String atMobiles;

    /**
     * 是否关闭提醒
     */
    private boolean disableNotify = false;

    private String monitorUrl;

    private Expression message;

    public DingTalkNotifier(InstanceRepository repository) {
        super(repository);
        this.message = parser.parseExpression(DEFAULT_MESSAGE, ParserContext.TEMPLATE_EXPRESSION);
    }

    @Override
    protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
        if (!this.disableNotify) {
            return Mono.fromRunnable(
                    () -> this.restTemplate.postForEntity(buildUrl(), this.createMessage(event, instance), Void.class));
        }
        return Mono.empty();
    }

    /**
     * 构建发送信息实体
     *
     * @param event
     * @param instance
     * @return
     */
    private HttpEntity<Map<String, Object>> createMessage(InstanceEvent event, Instance instance) {
        Map<String, Object> bodyJson = new HashMap<>(3);

        Map<String, String> params = new HashMap<>(2);
        String text = this.getText(event, instance);
        log.info(text);
        params.put("text", text);
        params.put("title", this.title);

        bodyJson.put("msgtype", this.msgType);
        bodyJson.put(this.msgType, params);

        if (StringUtils.isNotBlank(this.atMobiles)) {
            Map<String, Object> atParams = new HashMap<>(2);
            atParams.put("atMobiles", Arrays.asList(this.getAtMobileArr(this.atMobiles)));
            atParams.put("isAtAll", false);
            bodyJson.put("at", atParams);
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return new HttpEntity<>(bodyJson, headers);
    }

    protected String getText(InstanceEvent event, Instance instance) {
        Map<String, Object> root = new HashMap<>(6);
        root.put("event", event);
        root.put("instance", instance);
        root.put("lastStatus", getLastStatus(event.getInstance()));
        root.put("monitorUrl", this.monitorUrl);

        // 构建应用状态信息
        StatusInfo statusInfo = instance.getStatusInfo();
        if (statusInfo != null) {
            try {
                StringBuilder statusInfoStr = new StringBuilder("应用状态: " + statusInfo.getStatus() + ";");
                Map<String, Object> detail = statusInfo.getDetails();
                if (detail.containsKey(EX_KEY)) {
                    String exception = detail.get(EX_KEY) != null ? detail.get(EX_KEY).toString() : "";
                    String message = detail.get("message") != null ? detail.get("message").toString() : "";
                    statusInfoStr.append(" 异常: ").append(exception).append(";");
                    statusInfoStr.append(" 信息: ").append(message).append(";");
                } else {
                    for (String resName : detail.keySet()) {
                        Map<String, Object> info = (Map<String, Object>) detail.get(resName);
                        String resStatus = info.get("status").toString();
                        Map<String, Object> resDetail = (Map<String, Object>) info.get("details");
                        if (resDetail.containsKey(ER_KEY)) {
                            String errDetail = resDetail.get(ER_KEY) != null ? resDetail.get(ER_KEY).toString() : "";
                            statusInfoStr.append(" 错误详情: ").append(errDetail).append(" ");
                        } else {
                            if ("diskSpace".equalsIgnoreCase(resName)) {
                                statusInfoStr.append(" 磁盘存储: 状态: ").append(resStatus);
                                for (String diskKey : resDetail.keySet()) {
                                    if ("threshold".equalsIgnoreCase(diskKey)) {
                                        Integer diskValue = (Integer) resDetail.get(diskKey);
                                        statusInfoStr.append(" ").append(diskKey).append(": ").append(humanReadableByteCount(diskValue, true)).append(" ");
                                    } else {
                                        Long diskValue = (Long) resDetail.get(diskKey);
                                        statusInfoStr.append(" ").append(diskKey).append(": ").append(humanReadableByteCount(diskValue, true)).append(" ");
                                    }
                                }
                            } else if ("db".equalsIgnoreCase(resName)) {
                                statusInfoStr.append(" 数据库: 状态: " + resStatus);

                                for (String dbKey : resDetail.keySet()) {
                                    String dbValue = resDetail.get(dbKey).toString();
                                    statusInfoStr.append(" ").append(dbKey).append(": ").append(dbValue).append(" ");
                                }
                            } else if ("redis".equalsIgnoreCase(resName)) {
                                statusInfoStr.append(" redis: 状态: ").append(resStatus);

                                for (String redisKey : resDetail.keySet()) {
                                    String redisValue = resDetail.get(redisKey).toString();
                                    statusInfoStr.append(" ").append(redisKey).append(": ").append(redisValue).append(" ");
                                }
                            } else {
                                statusInfoStr.append(" ").append(resName).append(": 状态: ").append(resStatus);

                                for (String otherKey : resDetail.keySet()) {
                                    String redisValue = resDetail.get(otherKey).toString();
                                    statusInfoStr.append(" ").append(otherKey).append(": ").append(redisValue).append(" ");
                                }
                            }
                        }
                    }
                }
                root.put("statusInfo", statusInfoStr);
            } catch (Exception ex) {
                log.error(ex.getMessage(), ex);
                root.put("statusInfo", statusInfo.toString());
            }
        }

        StandardEvaluationContext context = new StandardEvaluationContext(root);
        context.addPropertyAccessor(new MapAccessor());
        return message.getValue(context, String.class);
    }

    /**
     * Formatting byte size to human readable format
     *
     * @param bytes
     * @param si
     * @return
     */
    private static String humanReadableByteCount(long bytes, boolean si) {
        int unit = si ? 1000 : 1024;
        if (bytes < unit) {
            return bytes + " B";
        }
        int exp = (int) (Math.log(bytes) / Math.log(unit));
        String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
        return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
    }

    /**
     * 构建@人
     *
     * @param s
     * @return
     */
    private String[] getAtMobileArr(String s) {
        return s.split(",");
    }

    /**
     * 构建请求链接
     *
     * @return
     */
    protected String buildUrl() {
        return String.format("%s?access_token=%s", this.apiUrl, this.accessToken);
    }

    public void setRestTemplate(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public String getApiUrl() {
        return apiUrl;
    }

    public void setApiUrl(String apiUrl) {
        this.apiUrl = apiUrl;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getAtMobiles() {
        return atMobiles;
    }

    public void setAtMobiles(String atMobiles) {
        this.atMobiles = atMobiles;
    }

    public boolean isDisableNotify() {
        return disableNotify;
    }

    public void setDisableNotify(boolean disableNotify) {
        this.disableNotify = disableNotify;
    }

    public String getMsgType() {
        return msgType;
    }

    public void setMsgType(String msgType) {
        this.msgType = msgType;
    }

    public String getMonitorUrl() {
        return monitorUrl;
    }

    public void setMonitorUrl(String monitorUrl) {
        this.monitorUrl = monitorUrl;
    }

    public void setMessage(String message) {
        this.message = parser.parseExpression(message, ParserContext.TEMPLATE_EXPRESSION);
    }
}
